Skip to content

feat: implement input sanitization and rate limiting middleware - #479

Merged
ritik4ever merged 1 commit into
ritik4ever:mainfrom
edehvictor:feature/rate-limiting-and-input-sanitization
Jun 25, 2026
Merged

feat: implement input sanitization and rate limiting middleware#479
ritik4ever merged 1 commit into
ritik4ever:mainfrom
edehvictor:feature/rate-limiting-and-input-sanitization

Conversation

@edehvictor

@edehvictor edehvictor commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Description

This pull request addresses two assigned tasks under the backend scope:
Input Sanitization via Zod Schema (Issue #211):

  • Validates and sanitizes campaign title and description payload properties.
  • Trims leading/trailing whitespace.
  • Escapes HTML tags in both fields during schema parsing to prevent injection and XSS attempts.
  • Rejects titles containing only whitespace.
  • Rejects payload elements containing malicious script tags (<script>) or SQL comment sequences (--, /*, */).
  • Implements a dedicated test suite under schemas.test.ts.

Rate Limiting Middleware (Issue #210):

  • Implemented separate rate limit limits based on request methods (GET/HEAD requests default to 120 req/min; POST/PUT/PATCH/DELETE write requests default to 20 req/min).
  • Rate limit parameters (RATE_LIMIT_WINDOW_MS, RATE_LIMIT_READ_LIMIT, RATE_LIMIT_WRITE_LIMIT) are dynamically configurable via environment variables.
  • Appends standard X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers to all responses.
  • Automatically handles 429 errors returning standard JSON AppError responses and sets the Retry-After header.
  • Created a dedicated unit test suite under rateLimiter.test.ts.

. General Build/Compilation Fix:

  • Fixed a compile-time bug in config.ts where contractId and sorobanRpcUrl were referenced on the export object but were not declared.

Verification

  • Unit test suites cover validation logic and rate limiting behavior.

Closes #206
Closes #207
Closes #210
Closes #211

Summary by CodeRabbit

  • New Features

    • Rate limits are now configurable, with different defaults for read and write requests.
    • Added support for additional environment-based connection settings.
  • Bug Fixes

    • Improved campaign text handling by trimming, sanitizing, and blocking risky input patterns.
    • Prevented duplicate rate-limit handling across multiple request layers.
  • Tests

    • Added coverage for rate limiting behavior and campaign input validation.

@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

@edehvictor is attempting to deploy a commit to the ritik4ever's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jun 25, 2026

Copy link
Copy Markdown

@edehvictor Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Backend config gains two environment-backed fields. Rate limiting now uses configurable read/write limits and method-based defaults. Campaign payload validation now rejects script and SQL comment patterns and sanitizes accepted strings. New tests cover the middleware and schema behavior.

Changes

Backend configuration

Layer / File(s) Summary
Config fields
backend/src/config.ts
config now includes contractId and sorobanRpcUrl from environment variables with fallback defaults.

Rate limiting middleware

Layer / File(s) Summary
Environment limits
backend/src/index.ts
RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_REQUESTS, and WRITE_RATE_LIMIT_MAX_REQUESTS now come from environment variables with fallback defaults.
Middleware behavior
backend/src/index.ts
applyRateLimit now selects read or write limits by request method, keys requests by IP and type, skips repeated processing, sets rate-limit headers, and is mounted globally with app.use(applyRateLimit()).
Middleware tests
backend/src/rateLimiter.test.ts
Vitest cases cover GET and POST header values and the limit-exceeded path with Retry-After.

Campaign payload sanitization

Layer / File(s) Summary
Sanitization helpers
backend/src/validation/schemas.ts
Local helpers escape <, >, and /, and detect <script tags and SQL comment sequences.
Schema validation pipeline
backend/src/validation/schemas.ts
title and description now add refinements for script tags and SQL comment patterns, then apply sanitizeInput.
Schema tests
backend/src/validation/schemas.test.ts
Vitest cases cover trimming, HTML escaping output, whitespace-only rejection, and injection-pattern rejection for title and description.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A bunny hopped through config springs,
Then twitched at rate-limit bells and rings.
I nibbled scripts, no trouble found,
And left clean carrots all around. 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers #210 and #211, but it does not implement #206 creator analytics or #207 Dockerfile refactors. Add the on-chain creator analytics and Dockerfile multi-stage refactors, or remove those issues from the linked set.
Out of Scope Changes check ⚠️ Warning The config.ts addition of contractId and sorobanRpcUrl is unrelated to the linked issues' stated scope. Either justify this config fix under a linked issue or move it to a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: input sanitization and rate limiting middleware.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
backend/src/validation/schemas.ts (1)

55-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

-- SQL-comment heuristic will reject legitimate text.

The -- branch matches common prose (e.g., "Project X -- Phase 2", em-dash usage), so valid titles/descriptions are silently rejected with a confusing error. Note that input filtering is not a reliable SQL-injection defense; parameterized queries / an ORM at the persistence layer are. Consider relaxing or removing this heuristic and relying on parameterized queries downstream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/validation/schemas.ts` at line 55, The SQL comment heuristic in
containsSqlComment is too broad because the `--` check rejects legitimate prose
and title text. Update the validation in schemas.ts by relaxing or removing the
`--`-based match, and keep the check focused on actual comment delimiters like
block comments if needed. Ensure the schema rules for the affected fields still
validate user input appropriately while relying on parameterized queries or the
persistence layer for SQL-injection protection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/index.ts`:
- Around line 65-67: The rate limit constants in index.ts are currently derived
with Number(...), which can turn invalid env values into NaN or 0 and break
throttling. Update the parsing for RATE_LIMIT_WINDOW_MS,
RATE_LIMIT_MAX_REQUESTS, and WRITE_RATE_LIMIT_MAX_REQUESTS to validate that the
env values are positive safe integers, and fall back to the existing defaults
when they are missing or invalid. Keep the fix localized to the rate-limit setup
near the RATE_LIMIT_* constants so the downstream header and limiter logic
always receives valid numbers.
- Around line 117-138: The rate limiting logic in the request handler leaves
expired entries in rateLimitBuckets indefinitely, which can cause unbounded
memory growth. Update the rate-limit path around the existing current/resetAt
handling to remove buckets whose resetAt has passed before computing headers or
updating the count, and consider pruning the key on each request when the window
has expired so stale IP/type entries do not accumulate.

In `@backend/src/rateLimiter.test.ts`:
- Around line 11-17: The rate limiter tests are reusing the same request object
and IP across calls, which causes applyRateLimit to carry over the
rateLimitedProcessed bypass flag and shared bucket state. Update the test setup
around mockReq and the repeated applyRateLimit calls to create a fresh request
object for each simulated HTTP request, and use a unique ip per test/case so
state does not leak between assertions. Reference the applyRateLimit test helper
usage and the beforeEach mockReq initialization when making the change.

In `@backend/src/validation/schemas.ts`:
- Around line 48-53: The sanitizeInput helper currently misses entity escaping
for ampersands and quotes, which lets entity-encoded payloads bypass
containsScriptTag and later render unsafely. Update sanitizeInput to escape "&"
first, then the other special characters, and include escaping for both
quotation marks; keep the change centered on sanitizeInput so the existing
validation flow still works. Also adjust the corresponding expectations in
schemas.test.ts to match the new escaped output.

---

Nitpick comments:
In `@backend/src/validation/schemas.ts`:
- Line 55: The SQL comment heuristic in containsSqlComment is too broad because
the `--` check rejects legitimate prose and title text. Update the validation in
schemas.ts by relaxing or removing the `--`-based match, and keep the check
focused on actual comment delimiters like block comments if needed. Ensure the
schema rules for the affected fields still validate user input appropriately
while relying on parameterized queries or the persistence layer for
SQL-injection protection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85d92105-9e78-4d59-87ee-19527daffe3b

📥 Commits

Reviewing files that changed from the base of the PR and between 922519c and 9085082.

📒 Files selected for processing (5)
  • backend/src/config.ts
  • backend/src/index.ts
  • backend/src/rateLimiter.test.ts
  • backend/src/validation/schemas.test.ts
  • backend/src/validation/schemas.ts

Comment thread backend/src/index.ts
Comment on lines +65 to +67
const RATE_LIMIT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS ?? 60000);
const RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_READ_LIMIT ?? process.env.RATE_LIMIT_MAX_REQUESTS ?? 120);
const WRITE_RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_WRITE_LIMIT ?? process.env.WRITE_RATE_LIMIT_MAX_REQUESTS ?? 20);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate env limits before using them.

Number(...) accepts invalid config as NaN or 0, which can make headers emit NaN and effectively bypass throttling. Parse positive safe integers and fall back when invalid.

Suggested fix
-const RATE_LIMIT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS ?? 60000);
-const RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_READ_LIMIT ?? process.env.RATE_LIMIT_MAX_REQUESTS ?? 120);
-const WRITE_RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_WRITE_LIMIT ?? process.env.WRITE_RATE_LIMIT_MAX_REQUESTS ?? 20);
+function parsePositiveInt(value: string | undefined, fallback: number): number {
+  const parsed = Number(value);
+  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
+}
+
+const RATE_LIMIT_WINDOW_MS = parsePositiveInt(process.env.RATE_LIMIT_WINDOW_MS, 60000);
+const RATE_LIMIT_MAX_REQUESTS = parsePositiveInt(
+  process.env.RATE_LIMIT_READ_LIMIT ?? process.env.RATE_LIMIT_MAX_REQUESTS,
+  120,
+);
+const WRITE_RATE_LIMIT_MAX_REQUESTS = parsePositiveInt(
+  process.env.RATE_LIMIT_WRITE_LIMIT ?? process.env.WRITE_RATE_LIMIT_MAX_REQUESTS,
+  20,
+);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const RATE_LIMIT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS ?? 60000);
const RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_READ_LIMIT ?? process.env.RATE_LIMIT_MAX_REQUESTS ?? 120);
const WRITE_RATE_LIMIT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_WRITE_LIMIT ?? process.env.WRITE_RATE_LIMIT_MAX_REQUESTS ?? 20);
function parsePositiveInt(value: string | undefined, fallback: number): number {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
}
const RATE_LIMIT_WINDOW_MS = parsePositiveInt(process.env.RATE_LIMIT_WINDOW_MS, 60000);
const RATE_LIMIT_MAX_REQUESTS = parsePositiveInt(
process.env.RATE_LIMIT_READ_LIMIT ?? process.env.RATE_LIMIT_MAX_REQUESTS,
120,
);
const WRITE_RATE_LIMIT_MAX_REQUESTS = parsePositiveInt(
process.env.RATE_LIMIT_WRITE_LIMIT ?? process.env.WRITE_RATE_LIMIT_MAX_REQUESTS,
20,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` around lines 65 - 67, The rate limit constants in
index.ts are currently derived with Number(...), which can turn invalid env
values into NaN or 0 and break throttling. Update the parsing for
RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_REQUESTS, and WRITE_RATE_LIMIT_MAX_REQUESTS
to validate that the env values are positive safe integers, and fall back to the
existing defaults when they are missing or invalid. Keep the fix localized to
the rate-limit setup near the RATE_LIMIT_* constants so the downstream header
and limiter logic always receives valid numbers.

Comment thread backend/src/index.ts
Comment on lines 117 to +138
const now = Date.now();
const current = rateLimitBuckets.get(key);

if (!current || now >= current.resetAt) {
rateLimitBuckets.set(key, {
count: 1,
resetAt: now + RATE_LIMIT_WINDOW_MS,
});
return next();
let count = 1;
let resetAt = now + RATE_LIMIT_WINDOW_MS;

if (current && now < current.resetAt) {
count = current.count + 1;
resetAt = current.resetAt;
}

if (current.count >= maxRequests) {
res.setHeader("X-RateLimit-Limit", String(maxRequests));
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, maxRequests - count)));
res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));

if (current && now < current.resetAt && current.count >= maxRequests) {
const retryAfterSec = Math.max(1, Math.ceil((current.resetAt - now) / 1000));
res.setHeader('Retry-After', String(retryAfterSec));
throw new AppError('Rate limit exceeded. Please retry shortly.', 429, 'RATE_LIMITED');
res.setHeader("Retry-After", String(retryAfterSec));
throw new AppError("Rate limit exceeded. Please retry shortly.", 429, "RATE_LIMITED");
}

current.count += 1;
rateLimitBuckets.set(key, current);
rateLimitBuckets.set(key, { count, resetAt });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prune expired buckets to avoid unbounded memory growth.

Each distinct IP/type key remains in rateLimitBuckets forever unless the same key returns. A botnet or spoofed-proxy scenario can grow this map without bound.

Suggested fix
+let lastRateLimitSweep = 0;
+
+function sweepExpiredRateLimitBuckets(now: number): void {
+  if (now - lastRateLimitSweep < RATE_LIMIT_WINDOW_MS) return;
+  lastRateLimitSweep = now;
+
+  for (const [bucketKey, bucket] of rateLimitBuckets.entries()) {
+    if (now >= bucket.resetAt) {
+      rateLimitBuckets.delete(bucketKey);
+    }
+  }
+}
+
     const key = `${req.ip}:${isWrite ? "write" : "read"}`;
     const now = Date.now();
+    sweepExpiredRateLimitBuckets(now);
     const current = rateLimitBuckets.get(key);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const now = Date.now();
const current = rateLimitBuckets.get(key);
if (!current || now >= current.resetAt) {
rateLimitBuckets.set(key, {
count: 1,
resetAt: now + RATE_LIMIT_WINDOW_MS,
});
return next();
let count = 1;
let resetAt = now + RATE_LIMIT_WINDOW_MS;
if (current && now < current.resetAt) {
count = current.count + 1;
resetAt = current.resetAt;
}
if (current.count >= maxRequests) {
res.setHeader("X-RateLimit-Limit", String(maxRequests));
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, maxRequests - count)));
res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));
if (current && now < current.resetAt && current.count >= maxRequests) {
const retryAfterSec = Math.max(1, Math.ceil((current.resetAt - now) / 1000));
res.setHeader('Retry-After', String(retryAfterSec));
throw new AppError('Rate limit exceeded. Please retry shortly.', 429, 'RATE_LIMITED');
res.setHeader("Retry-After", String(retryAfterSec));
throw new AppError("Rate limit exceeded. Please retry shortly.", 429, "RATE_LIMITED");
}
current.count += 1;
rateLimitBuckets.set(key, current);
rateLimitBuckets.set(key, { count, resetAt });
let lastRateLimitSweep = 0;
function sweepExpiredRateLimitBuckets(now: number): void {
if (now - lastRateLimitSweep < RATE_LIMIT_WINDOW_MS) return;
lastRateLimitSweep = now;
for (const [bucketKey, bucket] of rateLimitBuckets.entries()) {
if (now >= bucket.resetAt) {
rateLimitBuckets.delete(bucketKey);
}
}
}
const key = `${req.ip}:${isWrite ? "write" : "read"}`;
const now = Date.now();
sweepExpiredRateLimitBuckets(now);
const current = rateLimitBuckets.get(key);
let count = 1;
let resetAt = now + RATE_LIMIT_WINDOW_MS;
if (current && now < current.resetAt) {
count = current.count + 1;
resetAt = current.resetAt;
}
res.setHeader("X-RateLimit-Limit", String(maxRequests));
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, maxRequests - count)));
res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));
if (current && now < current.resetAt && current.count >= maxRequests) {
const retryAfterSec = Math.max(1, Math.ceil((current.resetAt - now) / 1000));
res.setHeader("Retry-After", String(retryAfterSec));
throw new AppError("Rate limit exceeded. Please retry shortly.", 429, "RATE_LIMITED");
}
rateLimitBuckets.set(key, { count, resetAt });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` around lines 117 - 138, The rate limiting logic in the
request handler leaves expired entries in rateLimitBuckets indefinitely, which
can cause unbounded memory growth. Update the rate-limit path around the
existing current/resetAt handling to remove buckets whose resetAt has passed
before computing headers or updating the count, and consider pruning the key on
each request when the window has expired so stale IP/type entries do not
accumulate.

Comment on lines +11 to +17
beforeEach(() => {
nextCalled = false;
headers = {};
mockReq = {
ip: "127.0.0.1",
method: "GET",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use fresh request objects for each simulated HTTP request.

applyRateLimit sets rateLimitedProcessed on the request, so the second and third calls reuse the bypass flag and never increment the bucket. Also use a unique IP per test to avoid leaking module-level bucket state between cases.

Suggested fix
 describe("Rate Limiter Middleware", () => {
   let mockReq: Partial<Request>;
   let mockRes: Partial<Response>;
   let nextCalled: boolean;
   let headers: Record<string, string>;
+  let testIpCounter = 0;
+  let testIp: string;
 
   beforeEach(() => {
     nextCalled = false;
     headers = {};
+    testIp = `127.0.0.${++testIpCounter}`;
     mockReq = {
-      ip: "127.0.0.1",
+      ip: testIp,
       method: "GET",
     };
@@
   it("should enforce rate limiting and throw 429 when limit is exceeded", () => {
-    mockReq.method = "POST";
+    const makePostReq = () => ({ ip: testIp, method: "POST" }) as Request;
     const middleware = applyRateLimit(2); // Set limit to 2 for testing
 
     // First request
-    middleware(mockReq as Request, mockRes as Response, next);
+    middleware(makePostReq(), mockRes as Response, next);
     expect(nextCalled).toBe(true);
 
     // Second request
     nextCalled = false;
-    middleware(mockReq as Request, mockRes as Response, next);
+    middleware(makePostReq(), mockRes as Response, next);
     expect(nextCalled).toBe(true);
 
     // Third request - should exceed limit
     nextCalled = false;
     expect(() => {
-      middleware(mockReq as Request, mockRes as Response, next);
+      middleware(makePostReq(), mockRes as Response, next);
     }).toThrow(/Rate limit exceeded/);

Also applies to: 53-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/rateLimiter.test.ts` around lines 11 - 17, The rate limiter tests
are reusing the same request object and IP across calls, which causes
applyRateLimit to carry over the rateLimitedProcessed bypass flag and shared
bucket state. Update the test setup around mockReq and the repeated
applyRateLimit calls to create a fresh request object for each simulated HTTP
request, and use a unique ip per test/case so state does not leak between
assertions. Reference the applyRateLimit test helper usage and the beforeEach
mockReq initialization when making the change.

Comment on lines +48 to +53
function sanitizeInput(val: string): string {
return val
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\//g, "&sol;");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical

Incomplete HTML escaping creates an entity-based XSS bypass

sanitizeInput escapes <, >, and / but fails to escape &. Consequently, entity-encoded payloads like &amp;#60;script&amp;#62; bypass the containsScriptTag check (which only matches literal <script) and are persisted unescaped. When rendered, &amp; decodes back to <, allowing script execution.

Additionally:

  1. The &amp; replacement must occur before escaping other entities to prevent double-encoding issues.
  2. Quotes (", ') are also missing for attribute context safety.
  3. Updates to schemas.test.ts (Lines 42-43) are required to reflect the corrected output.
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 48-50: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: val
.replace(/</g, "<")
.replace(/>/g, ">")
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 48-49: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: val
.replace(/</g, "<")
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/validation/schemas.ts` around lines 48 - 53, The sanitizeInput
helper currently misses entity escaping for ampersands and quotes, which lets
entity-encoded payloads bypass containsScriptTag and later render unsafely.
Update sanitizeInput to escape "&" first, then the other special characters, and
include escaping for both quotation marks; keep the change centered on
sanitizeInput so the existing validation flow still works. Also adjust the
corresponding expectations in schemas.test.ts to match the new escaped output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants